/*
 * $Author: isan.wong@gmail.com $
 * $Date: 2011-07-29 01:03:06 +0000 (Fri, 29 Jul 2011) $
 * $Rev: 13 $
 * $URL: http://capitalbio-smd.googlecode.com/svn/src/com/capitalbio/smd/base/Variation.java $
 */

package com.capitalbio.smd.base;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.biojava.bio.seq.DNATools;
import org.biojava.bio.symbol.IllegalSymbolException;
import org.biojava.bio.symbol.SimpleSymbolList;
import org.biojava.bio.symbol.Symbol;
import org.biojava.bio.symbol.SymbolList;

/**
 * <p>
 * Sequence variation at DNA level
 * </p>
 * 
 * 
 * <p>
 * <ul>
 * <li>
 * nucleotides are designated by the bases (in upper case);
 * <ul>
 * <li>
 * A (adenine)</li>
 * <li>
 * C (cytosine)</li>
 * <li>
 * G (guanine)</li>
 * <li>
 * T (thymidine)</li>
 * </ul>
 * </li>
 * <li>
 * nucleotide numbering
 * <ul>
 * <li>
 * coding DNA Reference Sequence
 * <ul>
 * <li>
 * there is no nucleotide 0</li>
 * <li>
 * nucleotide 1 is the A of the ATG-translation initiation codon</li>
 * <li>
 * the nucleotide 5' of the ATG-translation initiation codon is -1, the previous
 * -2, etc.</li>
 * <li>
 * the nucleotide 3' of the translation stop codon is *1, the next *2, etc.</li>
 * <li>
 * intronic nucleotides for a coding DNA reference sequence
 * <ul>
 * <li>
 * beginning of the intron; the number of the last nucleotide of the preceding
 * exon, a plus sign and the position in the intron, like c.77+1G, c.77+2T, etc.
 * </li>
 * <li>
 * end of the intron; the number of the first nucleotide of the following exon,
 * a minus sign and the position upstream in the intron, like c.78-1G.</li>
 * <li>
 * in the middle of the intron, numbering changes from "c.77+.." to "c.78-..";
 * for introns with an uneven number of nucleotides the central nucleotide is
 * the last described with a "+"</li>
 * <li>
 * <b>NOTE:</b> current opinions do not favour descriptions using the format
 * c.IVS1+1G and c.IVS1-2G</li>
 * </ul>
 * </li>
 * </ul>
 * </li>
 * <li>
 * genomic Reference Sequence
 * <ul>
 * <li>
 * nucleotide numbering is purely arbitrary and starts with 1 at the first
 * nucleotide of the database reference file</li>
 * <li>
 * no +, - or other signs are used</li>
 * <li>
 * the sequence should include all nucleotides covering the sequence (gene) of
 * interest and should start well 5' of the promoter of a gene</li>
 * <li>
 * when the complete genomic sequence is not known, a coding DNA reference
 * sequence should be used</li>
 * </ul>
 * </li>
 * <li>
 * for all descriptions the most 3' position possible is arbitrarily assigned to
 * have been changed</li>
 * </ul>
 * </li>
 * </ul>
 * </p>
 * 
 * @author Wang Lei
 */
public class Variation implements Comparable<Variation> {

	protected static Log log = LogFactory.getLog(Variation.class);

	private VariationType type;
	private VariationLocation location;
	private SymbolList source;
	private SymbolList target;
	private String variationStr;
	private boolean homozygous = false;

	/**
	 * Constructor
	 * 
	 * @param type
	 *            the variation type
	 * @param location
	 *            the variation location
	 * @param source
	 *            source symbol list
	 * @param target
	 *            target symbol list
	 */
	public Variation(VariationType type, VariationLocation location,
			SymbolList source, SymbolList target) {
		this.type = type;
		this.location = location;
		this.source = source;
		this.target = target;
	}

	/**
	 * Parse variation description string Only can parse del, ins and sub
	 * 
	 * @param string
	 *            such as c.299_300delAT etc.
	 * @return variation instance
	 * @throws IllegalSymbolException
	 */
	public static Variation parseVariation(String variationStr)
			throws IllegalSymbolException {

		log.debug("Parsing variation string: " + variationStr);
		Pattern variationPpattern = Pattern
				.compile("([cg]\\.[0-9_\\?\\*\\-]*)(.*)");
		Matcher variationMatcher = variationPpattern.matcher(variationStr);
		if (variationMatcher.matches()) {
			String locationStr = variationMatcher.group(1);
			VariationLocation location = VariationLocation
					.parseVariationLocation(locationStr);

			String descriptionStr = variationMatcher.group(2);
			VariationType type = VariationType
					.parseVariationType(descriptionStr);

			SymbolList source = null;
			SymbolList target = null;
			String sourceSeq = null;
			String targetSeq = null;
			switch (type) {
			case DELETION:
				target = new SimpleSymbolList(Util.getDNATokenization(), "");
				sourceSeq = descriptionStr.replaceFirst("del", "");
				source = new SimpleSymbolList(Util.getDNATokenization(),
						sourceSeq);
				break;
			case INSERTION:
				source = new SimpleSymbolList(Util.getDNATokenization(), "");
				targetSeq = descriptionStr.replaceFirst("ins", "");
				target = new SimpleSymbolList(Util.getDNATokenization(),
						targetSeq);
				break;
			case SUBSTITUTION:
				String[] seqs = descriptionStr.split(">");
				sourceSeq = seqs[0];
				targetSeq = seqs[1];
				source = new SimpleSymbolList(Util.getDNATokenization(),
						sourceSeq);
				target = new SimpleSymbolList(Util.getDNATokenization(),
						targetSeq);
				break;
			default:
				break;
			}
			Variation variation = new Variation(type, location, source, target);
			variation.variationStr = variationStr;

			return variation;

		} else {
			throw new IllegalArgumentException("Not a llegal variation string.");
		}
	}

	/**
	 * search variations that the sequence contains
	 * 
	 * @param variationLocationType
	 *            the type of the variation locaion
	 * @param offset
	 *            offset of the location of variations
	 * @param stdGene
	 *            standard gene
	 * @param sequence
	 *            sequence which has the same length with stdGene
	 */
	public static List<Variation> searchVariations(
			VariationLocation.VariationLocationType variationLocationType,
			int offset, StdGene stdGene, SymbolList seq0, SymbolList seq1) {
		// length check
		int seqLength = seq0.length();
		if (seqLength != seq1.length()) {
			throw new IllegalArgumentException(
					"Standard gene and sequence have different length.");
		}

		// searching...
		// ATTCATCGGATCGA-TTGCATGCTCA-AGTCGTAGACAGCT
		// ATTC--CGGATCGACTTGCATGC-ggCAGTCGTAGACAGCT
		// search difference first
		List<int[]> variationRegions = new ArrayList<int[]>(); // variation
																// region,
																// concerning
																// insertion
																// offset
		List<int[]> sequenceRegions = new ArrayList<int[]>(); // sequence region
		List<VariationType> variationTypes = new ArrayList<VariationType>();
		boolean newVariation = true;
		int insCount = 0;
		for (int i = 1; i <= seqLength; i++) {
			Symbol symbol0 = seq0.symbolAt(i);
			Symbol symbol1 = seq1.symbolAt(i);

			if (!symbol0.equals(symbol1)) {
				VariationType variationType = VariationType.parseVariationType(
						symbol0, symbol1);
				if (newVariation
						|| !variationType.equals(variationTypes
								.get(variationTypes.size() - 1))) {
					int[] variationRegion = new int[] { i + insCount,
							i + insCount };
					variationRegions.add(variationRegion);

					int[] sequenceRegion = new int[] { i, i };
					sequenceRegions.add(sequenceRegion);

					variationTypes.add(variationType);

				} else {
					variationRegions.get(variationRegions.size() - 1)[1] = i
							+ insCount;
					sequenceRegions.get(sequenceRegions.size() - 1)[1] = i;
				}
				newVariation = false;
			} else {
				newVariation = true;
			}

			if (symbol0 == Util.getGapSymbol()) {
				// count insertion of standard sequence, will be used in the
				// offset of variation
				insCount++;
			}
		}

		List<Variation> variations = new ArrayList<Variation>();
		for (int i = 0; i < variationRegions.size(); i++) {
			int[] variationRegion = variationRegions.get(i);
			int[] sequenceRegion = sequenceRegions.get(i);
			VariationType variationType = variationTypes.get(i);
			SymbolList source = seq0.subList(sequenceRegion[0],
					sequenceRegion[1]);
			SymbolList target = seq1.subList(sequenceRegion[0],
					sequenceRegion[1]);

			switch (variationType) {
			case DELETION:
				target = new SimpleSymbolList(DNATools.getDNA());
				break;
			case INSERTION:
				source = new SimpleSymbolList(DNATools.getDNA());
				break;
			}

			int start = variationRegion[0] + offset;
			int stop = variationRegion[1] + offset;

			VariationLocation variationLocation = null;
			if (variationLocationType
					.equals(VariationLocation.VariationLocationType.C)) {
				List<int[]> cdsRegions = stdGene.getCdsRegions();
				variationLocation = new VariationLocation(start, stop,
						cdsRegions);
			} else {
				variationLocation = new VariationLocation(
						variationLocationType, String.valueOf(start),
						String.valueOf(stop));
			}

			Variation variation = new Variation(variationType,
					variationLocation, source, target);
			variations.add(variation);
		}

		return variations;
	}

	/**
	 * search variations, combine variations with same type and location, and
	 * add homozygous information
	 * 
	 * @param seq0
	 *            standard sequence
	 * @param seqs
	 *            sequences
	 */
	public static List<Variation> searchVariations(
			VariationLocation.VariationLocationType variationLocationType,
			int offset, StdGene stdGene, SymbolList seq0, SymbolList... seqs) {

		// length check
		List<Variation> variations1 = new ArrayList<Variation>();
		for (SymbolList seq : seqs) {
			if (seq0.length() != seq.length()) {
				throw new IllegalArgumentException(
						"Standard gene and at least one sequence have different length.");
			}
			List<Variation> variations2 = searchVariations(
					variationLocationType, offset, stdGene, seq0, seq);
			variations1.addAll(variations2);
		}

		// group variations by type & location
		Map<String, List<Variation>> groups = new HashMap<String, List<Variation>>();
		for (Variation variation : variations1) {
			String variationStr = variation.toString();
			if (!groups.containsKey(variationStr)) {
				groups.put(variationStr, new ArrayList<Variation>());
			}
			groups.get(variationStr).add(variation);
		}

		List<Variation> variations = new ArrayList<Variation>();
		for (List<Variation> groupedVariations : groups.values()) {
			Variation variation = groupedVariations.get(0);
			// homozygous variation
			if (groupedVariations.size() > 1) {
				variation.setHomozygous(true);
			}
			variations.add(variation);
		}

		Collections.sort(variations);
		return variations;
	}

	/**
	 * Variation type
	 * 
	 * @return variation type
	 */
	public VariationType getType() {
		return type;
	}

	/**
	 * Variation location
	 * 
	 * @return variation location
	 */
	public VariationLocation getLocation() {
		return location;
	}

	/**
	 * Judge if two variations can be joined.
	 * Two variations can be joined if their types, homyzygous are same, and they are ajacent on location.
	 * @param variation another variation
	 * @return true /false
	 */
	public boolean canJoin(Variation variation) {
		log.debug("Judging if two variations can be joined: " + toString() + variation.toString());
		if(type.equals(variation.getType())
				&& homozygous == variation.isHomozygous()
				&& location.isAdjacent(variation.getLocation())) {
			log.debug("Result: Can be joined");
			return true;
		}
		log.debug("Result: Can NOT be joined");
		return false;
	}
	
	/**
	 * join this and variation in order.
	 * @param variation
	 * @return this + variation
	 */
	public Variation join(Variation variation) {
		VariationLocation newLocation = location.join(variation.getLocation());
		SymbolList newSource = null;
		SymbolList newTarget = null;
		try {
			newSource = new SimpleSymbolList(Util.getDNATokenization(), source.seqString() + variation.getSource().seqString());
			newTarget = new SimpleSymbolList(Util.getDNATokenization(), target.seqString() + variation.getTarget().seqString());
		} catch (IllegalSymbolException e) {
			log.error(e);
		}
		
		Variation newVariation = new Variation(type, newLocation, newSource, newTarget);
		return newVariation;
	}
	
	/**
	 * variation string
	 * //TODO this method is not completed yet.
	 * 
	 * @return variation string
	 */
	public String toString() {
		if (variationStr == null) {
			StringBuilder variationStrBuilder = new StringBuilder();
			variationStrBuilder.append(location.toString());

			if (type.equals(VariationType.DELETION)) {
				variationStrBuilder.append("del");
			} else if (type.equals(VariationType.INSERTION)) {
				variationStrBuilder.append("ins");
			}

			variationStrBuilder.append(source.seqString().toUpperCase());

			if (type.equals(VariationType.SUBSTITUTION)) {
				variationStrBuilder.append(">");
			}

			variationStrBuilder.append(target.seqString().toUpperCase());

			variationStr = variationStrBuilder.toString();
		}
		return variationStr;
	}

	/**
	 * is the variation homozygous? only meaningful for more than 2 sequences
	 * 
	 * @return if the variation is homozygous
	 */
	public boolean isHomozygous() {
		return homozygous;
	}

	public void setHomozygous(boolean homozygous) {
		this.homozygous = homozygous;
	}

	@Override
	public int compareTo(Variation variation) {
		if (this == variation) {
			return 0;
		}

		return this.location.compareTo(variation.getLocation());
	}
	
	public SymbolList getSource() {
		return source;
	}

	public void setSource(SymbolList source) {
		this.source = source;
	}

	public SymbolList getTarget() {
		return target;
	}

	public void setTarget(SymbolList target) {
		this.target = target;
	}

	public void setType(VariationType type) {
		this.type = type;
	}

	public void setLocation(VariationLocation location) {
		this.location = location;
	}

}